Исследование рынка заведений общественного питания г. Москва. Поиск интересных особенностей, которые в будущем помогут в выборе подходящего инвесторам места для открытия новых ресторанов.
Датасет с заведениями общественного питания Москвы, составлен на основе данных сервисов Яндекс Карты и Яндекс Бизнес (актуальность информации: лето 2022 года).
! pip install folium
Requirement already satisfied: folium in d:\anaconda\lib\site-packages (0.14.0) Requirement already satisfied: numpy in d:\anaconda\lib\site-packages (from folium) (1.21.5) Requirement already satisfied: jinja2>=2.9 in d:\anaconda\lib\site-packages (from folium) (2.11.3) Requirement already satisfied: requests in d:\anaconda\lib\site-packages (from folium) (2.27.1) Requirement already satisfied: branca>=0.6.0 in d:\anaconda\lib\site-packages (from folium) (0.6.0) Requirement already satisfied: MarkupSafe>=0.23 in d:\anaconda\lib\site-packages (from jinja2>=2.9->folium) (2.0.1) Requirement already satisfied: charset-normalizer~=2.0.0 in d:\anaconda\lib\site-packages (from requests->folium) (2.0.4) Requirement already satisfied: certifi>=2017.4.17 in d:\anaconda\lib\site-packages (from requests->folium) (2021.10.8) Requirement already satisfied: idna<4,>=2.5 in d:\anaconda\lib\site-packages (from requests->folium) (3.3) Requirement already satisfied: urllib3<1.27,>=1.21.1 in d:\anaconda\lib\site-packages (from requests->folium) (1.26.9)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from plotly import graph_objects as go
import plotly.express as px
import folium
from folium import Marker, Map, Choropleth
from folium.plugins import MarkerCluster
from folium.features import CustomIcon
import urllib.request, json
import warnings
warnings.filterwarnings('ignore')
sns.set_palette('pastel')
sns.set_style('whitegrid')
palette = px.colors.qualitative.Pastel1
moscow_lat, moscow_lng = 55.751244, 37.618423
try:
df = pd.read_csv('Datasets/moscow_places.csv')
except:
df = pd.read_csv('https://code.s3.yandex.net/datasets/moscow_places.csv')
try:
with open('C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson', 'r') as f:
geo_json = json.load(f)
state_geo = 'C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson'
except:
with urllib.request.urlopen("https://code.s3.yandex.net/data-analyst/admin_level_geomap.geojson") as url:
geo_json = json.load(url)
state_geo = 'https://code.s3.yandex.net/data-analyst/admin_level_geomap.geojson'
df.head()
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 |
Описание наименований столбцов:
name — название заведения;
address — адрес заведения;
category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
hours — информация о днях и часах работы;
lat — широта географической точки, в которой находится заведение;
lng — долгота географической точки, в которой находится заведение;
rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:
middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»
middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»
chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым:
district — административный район, в котором находится заведение, например Центральный административный округ;
seats — количество посадочных мест.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
Некоторые столбцы содержат пропущенные значения. Также данные необходимо проверить на наличие дубликатов и некорректных значений.
В связи с этим проводится предобработка данных.
df['category'].unique()
array(['кафе', 'ресторан', 'кофейня', 'пиццерия', 'бар,паб',
'быстрое питание', 'булочная', 'столовая'], dtype=object)
df['district'].unique()
array(['Северный административный округ',
'Северо-Восточный административный округ',
'Северо-Западный административный округ',
'Западный административный округ',
'Центральный административный округ',
'Восточный административный округ',
'Юго-Восточный административный округ',
'Южный административный округ',
'Юго-Западный административный округ'], dtype=object)
df['price'].unique()
array([nan, 'выше среднего', 'средние', 'высокие', 'низкие'], dtype=object)
df['chain'].unique()
array([0, 1], dtype=int64)
Неявных дубликатов в столбцах с категориальными данными не обнаружено
Далее проводится предобработка столбцов с названием и адресом
to_change = ['name', 'address']
for col in to_change:
for index in range(len(df[col])):
df[col][index] = df[col][index].lower()
for index in range(len(df[col])):
df['address'][index] = df['address'][index].replace('москва, ', '')
sum(df[['name', 'address']].duplicated())
4
Найдено 4 заведения с одним названием и адресом, вероятно это одни и те же заведения, которые были записаны по-разному
df[df[['name', 'address']].duplicated(keep=False) == True]
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 189 | кафе | кафе | парк ангарские пруды | Северный административный округ | ежедневно, 09:00–23:00 | 55.880327 | 37.530786 | 3.2 | NaN | NaN | NaN | NaN | 0 | NaN |
| 215 | кафе | кафе | парк ангарские пруды | Северный административный округ | ежедневно, 10:00–22:00 | 55.881438 | 37.531848 | 3.2 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1430 | more poke | ресторан | волоколамское шоссе, 11, стр. 2 | Северный административный округ | ежедневно, 09:00–21:00 | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | 0 | 188.0 |
| 1511 | more poke | ресторан | волоколамское шоссе, 11, стр. 2 | Северный административный округ | пн-чт 09:00–18:00; пт,сб 09:00–21:00; вс 09:00... | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | 1 | 188.0 |
| 2211 | раковарня клешни и хвосты | ресторан | проспект мира, 118 | Северо-Восточный административный округ | ежедневно, 12:00–00:00 | 55.810553 | 37.638161 | 4.4 | NaN | NaN | NaN | NaN | 0 | 150.0 |
| 2420 | раковарня клешни и хвосты | бар,паб | проспект мира, 118 | Северо-Восточный административный округ | пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00... | 55.810677 | 37.638379 | 4.4 | NaN | NaN | NaN | NaN | 1 | 150.0 |
| 3091 | хлеб да выпечка | булочная | ярцевская улица, 19 | Западный административный округ | ежедневно, 09:00–22:00 | 55.738886 | 37.411648 | 4.1 | NaN | NaN | NaN | NaN | 1 | 276.0 |
| 3109 | хлеб да выпечка | кафе | ярцевская улица, 19 | Западный административный округ | NaN | 55.738449 | 37.410937 | 4.1 | NaN | NaN | NaN | NaN | 0 | 276.0 |
Так и есть, 4 заведения имеют по 2 записи.
Любопытно, что для некоторых из них значения category и chain отличается.
Необходимо выяснить, какие из записей оставить.
df[df['name'] == 'more poke']
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1430 | more poke | ресторан | волоколамское шоссе, 11, стр. 2 | Северный административный округ | ежедневно, 09:00–21:00 | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | 0 | 188.0 |
| 1511 | more poke | ресторан | волоколамское шоссе, 11, стр. 2 | Северный административный округ | пн-чт 09:00–18:00; пт,сб 09:00–21:00; вс 09:00... | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | 1 | 188.0 |
| 6088 | more poke | ресторан | духовской переулок, 19 | Южный административный округ | ежедневно, 10:00–22:00 | 55.704177 | 37.612889 | 4.4 | NaN | NaN | NaN | NaN | 1 | NaN |
df[df['name'] == 'раковарня клешни и хвосты']
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2211 | раковарня клешни и хвосты | ресторан | проспект мира, 118 | Северо-Восточный административный округ | ежедневно, 12:00–00:00 | 55.810553 | 37.638161 | 4.4 | NaN | NaN | NaN | NaN | 0 | 150.0 |
| 2420 | раковарня клешни и хвосты | бар,паб | проспект мира, 118 | Северо-Восточный административный округ | пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00... | 55.810677 | 37.638379 | 4.4 | NaN | NaN | NaN | NaN | 1 | 150.0 |
| 7270 | раковарня клешни и хвосты | бар,паб | братиславская улица, 12 | Юго-Восточный административный округ | пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00... | 55.659744 | 37.752984 | 4.9 | средние | Цена бокала пива:150–250 ₽ | NaN | NaN | 1 | 40.0 |
df[df['name'] == 'хлеб да выпечка']
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3091 | хлеб да выпечка | булочная | ярцевская улица, 19 | Западный административный округ | ежедневно, 09:00–22:00 | 55.738886 | 37.411648 | 4.1 | NaN | NaN | NaN | NaN | 1 | 276.0 |
| 3109 | хлеб да выпечка | кафе | ярцевская улица, 19 | Западный административный округ | NaN | 55.738449 | 37.410937 | 4.1 | NaN | NaN | NaN | NaN | 0 | 276.0 |
| 7937 | хлеб да выпечка | кофейня | каширское шоссе, 61г | Южный административный округ | ежедневно, 09:00–22:00 | 55.621379 | 37.714108 | 4.5 | NaN | NaN | NaN | NaN | 1 | NaN |
Заведения являются сетевыми, соответственно, необходимо оставить записи, где chain = 1
df = df.drop([189, 1430, 2211, 3109]).reset_index()
Данные приведены к единому виду, дубликаты удалены.
Далее обрабатываются пропуски:
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8402 entries, 0 to 8401 Data columns (total 15 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 index 8402 non-null int64 1 name 8402 non-null object 2 category 8402 non-null object 3 address 8402 non-null object 4 district 8402 non-null object 5 hours 7867 non-null object 6 lat 8402 non-null float64 7 lng 8402 non-null float64 8 rating 8402 non-null float64 9 price 3315 non-null object 10 avg_bill 3816 non-null object 11 middle_avg_bill 3149 non-null float64 12 middle_coffee_cup 535 non-null float64 13 chain 8402 non-null int64 14 seats 4792 non-null float64 dtypes: float64(6), int64(2), object(7) memory usage: 984.7+ KB
Пропущенные значения столбца price могут быть заполнены на основании даныых middle_avg_bill и middle_coffee_cup
Определяем распределение среднего счета и средней цены чашки кофе:
for cat in df['price'].unique():
median_bill = df.query('price == @cat')['middle_avg_bill'].quantile([0.25,0.5,0.75])
print(cat)
print(median_bill)
print('--------------------')
nan 0.25 NaN 0.50 NaN 0.75 NaN Name: middle_avg_bill, dtype: float64 -------------------- выше среднего 0.25 1250.0 0.50 1250.0 0.75 1500.0 Name: middle_avg_bill, dtype: float64 -------------------- средние 0.25 350.0 0.50 500.0 0.75 850.0 Name: middle_avg_bill, dtype: float64 -------------------- высокие 0.25 1750.0 0.50 2000.0 0.75 2500.0 Name: middle_avg_bill, dtype: float64 -------------------- низкие 0.25 150.0 0.50 180.0 0.75 250.0 Name: middle_avg_bill, dtype: float64 --------------------
for cat in df['price'].unique():
median_coffee = df.query('price == @cat')['middle_coffee_cup'].quantile([0.25,0.5,0.75])
print(cat)
print(median_coffee)
print('--------------------')
nan 0.25 NaN 0.50 NaN 0.75 NaN Name: middle_coffee_cup, dtype: float64 -------------------- выше среднего 0.25 176.5 0.50 203.0 0.75 229.5 Name: middle_coffee_cup, dtype: float64 -------------------- средние 0.25 160.00 0.50 200.00 0.75 255.75 Name: middle_coffee_cup, dtype: float64 -------------------- высокие 0.25 250.0 0.50 250.0 0.75 250.0 Name: middle_coffee_cup, dtype: float64 -------------------- низкие 0.25 110.0 0.50 139.0 0.75 154.0 Name: middle_coffee_cup, dtype: float64 --------------------
На основании этих данных устанавливаем границы диапазонов для каждой ценовой категории:
avg_prices = pd.DataFrame({'Категория':df['price'].unique(),
'min_avg_bill':[None, 1001, 301, 1701, 0],
'max_avg_bill':[None, 1700, 1000, 999999, 300],
'min_avg_coffee':[None, 201, 156, 241, 0],
'max_avg_coffee':[None, 240, 200, 999999, 155]}
)
avg_prices
| Категория | min_avg_bill | max_avg_bill | min_avg_coffee | max_avg_coffee | |
|---|---|---|---|---|---|
| 0 | NaN | NaN | NaN | NaN | NaN |
| 1 | выше среднего | 1001.0 | 1700.0 | 201.0 | 240.0 |
| 2 | средние | 301.0 | 1000.0 | 156.0 | 200.0 |
| 3 | высокие | 1701.0 | 999999.0 | 241.0 | 999999.0 |
| 4 | низкие | 0.0 | 300.0 | 0.0 | 155.0 |
Далее заполняем датафрейм в соответствии с определенными выше границами:
low = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][4],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][4])]['price'].index)
med = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][2],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][2])]['price'].index)
med_high = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][1],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][1])]['price'].index)
high = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_avg_bill'] >= avg_prices['min_avg_bill'][3],
df['middle_avg_bill'] <= avg_prices['max_avg_bill'][3])]['price'].index)
for index in low:
df['price'][index] = 'низкие'
for index in med:
df['price'][index] = 'средние'
for index in med_high:
df['price'][index] = 'выше среднего'
for index in high:
df['price'][index] = 'высокие'
low_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][4],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][4])]['price'].index)
med_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][2],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][2])]['price'].index)
med_high_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][1],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][1])]['price'].index)
high_coffee = list(df[np.logical_and(df['price'].isnull() == True,
df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][3],
df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][3])]['price'].index)
for index in low_coffee:
df['price'][index] = 'низкие'
for index in med_coffee:
df['price'][index] = 'средние'
for index in med_high_coffee:
df['price'][index] = 'выше среднего'
for index in high_coffee:
df['price'][index] = 'высокие'
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8402 entries, 0 to 8401 Data columns (total 15 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 index 8402 non-null int64 1 name 8402 non-null object 2 category 8402 non-null object 3 address 8402 non-null object 4 district 8402 non-null object 5 hours 7867 non-null object 6 lat 8402 non-null float64 7 lng 8402 non-null float64 8 rating 8402 non-null float64 9 price 4042 non-null object 10 avg_bill 3816 non-null object 11 middle_avg_bill 3149 non-null float64 12 middle_coffee_cup 535 non-null float64 13 chain 8402 non-null int64 14 seats 4792 non-null float64 dtypes: float64(6), int64(2), object(7) memory usage: 984.7+ KB
В столбце price удалось заполнить около 700 значений.
Остальные значения заполнить не удается, поэтому оставляем пустыми.
Из столбца с адресом извлекается название улицы для последующего анализа и записывается в отдельный столбец.
df['street'] = df['address']
for index in range(len(df)):
df['street'][index] = df['street'][index].split(',')[0].strip()
Также из информации о часах работы извлекаются данные о заведениях, работающих ежедневно и круглосуточно.
df['is_24/7'] = 0
for index in range(len(df['hours'])):
if df['hours'][index] == 'ежедневно, круглосуточно':
df['is_24/7'][index] = 1
df['is_24/7'].sum()
730
df.head()
| index | name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | is_24/7 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | wowфли | кафе | улица дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN | улица дыбенко | 0 |
| 1 | 1 | четыре комнаты | ресторан | улица дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 | улица дыбенко | 0 |
| 2 | 2 | хазри | кафе | клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 | клязьминская улица | 0 |
| 3 | 3 | dormouse coffee shop | кофейня | улица маршала федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | средние | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN | улица маршала федоренко | 0 |
| 4 | 4 | иль марко | пиццерия | правобережная улица, 1б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 | правобережная улица | 0 |
Данные обработаны и дополнены, можно приступать к анализу.
category_pivot = df.pivot_table(index='category',
values='name',
aggfunc='count').sort_values(by='name', ascending=False)
category_pivot['percent'] = round(category_pivot['name'] / category_pivot['name'].sum() * 100, 2)
category_pivot
| name | percent | |
|---|---|---|
| category | ||
| кафе | 2376 | 28.28 |
| ресторан | 2041 | 24.29 |
| кофейня | 1413 | 16.82 |
| бар,паб | 765 | 9.10 |
| пиццерия | 633 | 7.53 |
| быстрое питание | 603 | 7.18 |
| столовая | 315 | 3.75 |
| булочная | 256 | 3.05 |
plt.figure(figsize=(12, 5))
sns.barplot(x=category_pivot['percent'],
y=category_pivot.index
)
plt.title('Количество заведений каждого типа (относительные величины)')
plt.ylabel('Категория')
plt.xlabel('Количество заведений (%)');
Из табличных и графических данных видно, что наибольшую долю составляют кафе и рестораны, также значительную часть заведений занимают кофейни.
plt.figure(figsize=(12, 5))
sns.boxplot(y='category',
x='seats',
data=df,
order=df.groupby('category')['seats'].median().sort_values(ascending=False).index
)
plt.title('Зависимость количества мест от типа заведения')
plt.ylabel('Категория')
plt.xlabel('Количество мест');
На графике указано количество мест в заведении каждой категории.
Заметно, что бары и рестораны в среднем наиболее вместительные, а пекарни и пиццерии наоборот.
Также заметно, что во всех категориях присутствуют "выбросы" - заведения с большим количеством мест.
Ниже представлено две диаграммы:
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'],
values=[df.query('chain == 1')['name'].count(),
df.query('chain == 0')['name'].count()],
marker_colors=palette,
title = 'Соотношение количества сетевых и не сетевых заведений (по количеству точек)'
)
]
)
pie_category.show()
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'],
values=[df.query('chain == 1')['name'].nunique(),
df.query('chain == 0')['name'].nunique()],
marker_colors=palette,
title = 'Соотношение количества сетевых и не сетевых заведений (по количеству наименований)'
)
]
)
pie_category.show()
Как видно, сетей всего 13%, тем не менее, они занимают почти 40% рынка.
Для оценки данного распределения подсчитываются все заведения каждой категории, а также количество сетевых.
Далее находится соотношение (условно - вероятность того, что заведение определеннй категории является сетевым.)
category_pivot_chains = df.query('chain == 1').pivot_table(index='category',
values='name',
aggfunc='count').sort_values(by='name', ascending=False)
category_pivot = category_pivot.drop(['percent'], axis=1)
category_pivot_chains = category_pivot.merge(category_pivot_chains, on='category')
category_pivot_chains
| name_x | name_y | |
|---|---|---|
| category | ||
| кафе | 2376 | 779 |
| ресторан | 2041 | 730 |
| кофейня | 1413 | 720 |
| бар,паб | 765 | 169 |
| пиццерия | 633 | 330 |
| быстрое питание | 603 | 232 |
| столовая | 315 | 88 |
| булочная | 256 | 157 |
category_pivot_chains.columns=('all_places', 'chain_places')
category_pivot_chains['chain_percent'] = round(category_pivot_chains['chain_places'] / \
category_pivot_chains['all_places'], 2
)
category_pivot_chains = category_pivot_chains.sort_values(by='chain_percent',
ascending=False
)
category_pivot_chains
| all_places | chain_places | chain_percent | |
|---|---|---|---|
| category | |||
| булочная | 256 | 157 | 0.61 |
| пиццерия | 633 | 330 | 0.52 |
| кофейня | 1413 | 720 | 0.51 |
| быстрое питание | 603 | 232 | 0.38 |
| ресторан | 2041 | 730 | 0.36 |
| кафе | 2376 | 779 | 0.33 |
| столовая | 315 | 88 | 0.28 |
| бар,паб | 765 | 169 | 0.22 |
plt.figure(figsize=(12, 5))
sns.barplot(x=category_pivot_chains['chain_percent'],
y=category_pivot_chains.index
)
plt.title('Зависимость количества сетевых заведений от типа (относительные величины)')
plt.ylabel('Категории')
plt.xlabel('Количество сетевых заведений');
Чаще всего булочные, пиццерии и кофейни являются сетевыми (небольшие заведения), в то время как бары и столовые чаще имеют только одну точку.
Отбираются самые крупные сети по количеству заведений.
top_15_chains = df.query('chain == 1').pivot_table(index ='name',
values='chain',
aggfunc='count'
).sort_values(by='chain',
ascending=False
).head(15)
top_15_chains.columns = ['places_total']
top_15_chains
| places_total | |
|---|---|
| name | |
| шоколадница | 120 |
| домино'с пицца | 76 |
| додо пицца | 74 |
| one price coffee | 71 |
| яндекс лавка | 69 |
| cofix | 65 |
| prime | 50 |
| хинкальная | 44 |
| кофепорт | 42 |
| кулинарная лавка братьев караваевых | 39 |
| теремок | 38 |
| чайхана | 37 |
| cofefest | 32 |
| буханка | 32 |
| му-му | 27 |
plt.figure(figsize=(6, 10))
sns.barplot(x=top_15_chains['places_total'],
y=top_15_chains.index.get_level_values(0)
)
plt.title('Топ-15 сетей по количеству заведений')
plt.ylabel('Название сети')
plt.xlabel('Количество заведение');
Шоколадница - самая многочисленная сеть.
Также можно заметить, что в топе в основном кофейни.
Посмотрим количество сетей каждой категории:
top_15_chains_data = df.query('name in @top_15_chains.index')
plt.figure(figsize=(12, 5))
sns.countplot(y='category',
data=top_15_chains_data,
order=top_15_chains_data.groupby('category')['name'].count().sort_values(ascending=False).index
)
plt.title('Количество заведений каждой категории среди Топ-15 сетей')
plt.ylabel('Категория')
plt.xlabel('Количество заведений');
Среди заведений Топ-15 сетей преобладают кофейни
plt.figure(figsize=(7, 15))
sns.countplot(y='name',
data=top_15_chains_data,
hue='category'
)
plt.title('Типы заведений среди Топ-15 сетей')
plt.ylabel('Название сети')
plt.xlabel('Количество заведение');
Большинство из представленных сетей имеют определенный фокус - 5 сетей кофеен, 3 сети ресторанов, 2 сети пиццерий, 1 сеть кафе и 1 сеть булочных.
Также встречаются и разнонаправленные сети, такие как Му-Му
plt.figure(figsize=(12, 5))
sns.countplot(y='district',
data=df,
order=df.groupby('district')['name'].count().sort_values(ascending=False).index
)
plt.title('Распределение заведений по административным округам')
plt.ylabel('Административный округ')
plt.xlabel('Количество заведений');
district_df = df.groupby('district', as_index=False)['name'].agg('count')
district_df
| district | name | |
|---|---|---|
| 0 | Восточный административный округ | 798 |
| 1 | Западный административный округ | 850 |
| 2 | Северный административный округ | 898 |
| 3 | Северо-Восточный административный округ | 890 |
| 4 | Северо-Западный административный округ | 409 |
| 5 | Центральный административный округ | 2242 |
| 6 | Юго-Восточный административный округ | 714 |
| 7 | Юго-Западный административный округ | 709 |
| 8 | Южный административный округ | 892 |
m_districts_total = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=district_df,
columns=['district', 'name'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Количество заведений по районам',
).add_to(m_districts_total);
m_districts_total
Неудивительно, что в центе заведений общественного питания почти втрое больше.
Остальные районы, кроме Северо-Западного содержат примерно одинаковое количество точек.
Распределение по категориям внутри каждого района
plt.figure(figsize=(6, 12))
sns.countplot(y='district',
data=df,
hue='category',
order=df.groupby('district')['name'].count().sort_values(ascending=False).index
)
plt.title('Распределение заведений каждой категории по административным округам')
plt.ylabel('Административный округ, Категория')
plt.xlabel('Количество заведений');
Соотношение категорий похоже для всех районов.
Однако, в центральном заметно превалирование количества ресторанов, что объясняется престижностью локации и, следовательно, классом заведений.
Изучается зависимость рейтинга от различных факторов:
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])
sns.barplot(x='category',
y='rating',
data = df,
order=df.groupby('category')['rating'].mean().sort_values(ascending=False).index
)
plt.title('Средние рейтинги для каждой категории заведений')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);
Закономерно, точки быстрого питания имеют более низкие рейтинги, т.к. их фокус заключается в скорости обслуживания.
Бары и рестораны наоборот, стараются сделать посещение максимально комфортным, поэтому в среднем имеют более высокие рейтинги.
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])
sns.barplot(x = 'category',
y='rating',
data = df,
hue='chain',
order=df.groupby('category')['rating'].mean().sort_values(ascending=False).index
)
plt.title('Средние рейтинги для каждой категории заведений (сравнение сетевых и не сетевых)')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);
Интересное наблюдение - среди кафе пользователи предпочитают сетевые заведения, в то время как при посещении ресторанов или кофеен отдают предпочтение локальным точкам.
rating_df = df.groupby('district', as_index=False)['rating'].agg('mean').round(2)
rating_df
| district | rating | |
|---|---|---|
| 0 | Восточный административный округ | 4.17 |
| 1 | Западный административный округ | 4.18 |
| 2 | Северный административный округ | 4.24 |
| 3 | Северо-Восточный административный округ | 4.15 |
| 4 | Северо-Западный административный округ | 4.21 |
| 5 | Центральный административный округ | 4.38 |
| 6 | Юго-Восточный административный округ | 4.10 |
| 7 | Юго-Западный административный округ | 4.17 |
| 8 | Южный административный округ | 4.18 |
m_chor_rating = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=rating_df,
columns=['district', 'rating'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Медианный рейтинг заведений по районам',
).add_to(m_chor_rating);
m_chor_rating
Средний рейтинг заведений в центре выше, однако разброс данных незначительный (всего 0,2 балла)
Все указанные в датасете заведения отмечены на карте ниже.
Для удобства навигации, каждая категория заведений имеет свою иконку.
m_clust = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
icons = {'кафе':'https://img.icons8.com/color/256/restaurant-building.png',
'ресторан':'https://img.icons8.com/external-flaticons-flat-flat-icons/256/external-restaurant-vegan-and-vegetarian-flaticons-flat-flat-icons.png',
'кофейня':'https://img.icons8.com/office/256/coffee.png',
'пиццерия':'https://img.icons8.com/color/256/pizza.png',
'бар,паб':'https://img.icons8.com/external-wanicon-lineal-color-wanicon/256/external-pub-st-patrick-day-wanicon-lineal-color-wanicon.png',
'быстрое питание':'https://img.icons8.com/external-kosonicon-lineal-color-kosonicon/256/external-fast-food-back-to-school-kosonicon-lineal-color-kosonicon.png',
'булочная':'https://img.icons8.com/color/256/bakery.png',
'столовая':'https://img.icons8.com/color-glass/256/dining-room.png'
}
marker_cluster = MarkerCluster().add_to(m_clust)
def create_clusters(row):
for cat in df['category'].unique():
if row['category'] == cat:
icon_url = icons.get(cat)
icon = CustomIcon(icon_url, icon_size=(30, 30))
Marker([row['lat'],
row['lng']],
popup=f"{row['name']} {row['rating']}",
icon=icon,
).add_to(marker_cluster)
df.apply(create_clusters, axis=1);
m_clust
Исследуется, на каких улицах находися наибольшее количество заведений общественного питания
top_15_streets = df.pivot_table(index='street',
values='name',
aggfunc='count'
).sort_values(by='name',
ascending=False
).head(15)
plt.figure(figsize=(12, 5))
sns.barplot(x=top_15_streets['name'],
y=top_15_streets.index
)
plt.title('Топ-15 улиц по количеству заведений общественного питания')
plt.xlabel('Количество точек')
plt.ylabel('Улица');
Разумеется, в Топ-15 вошли крупные магистрали с большим потоком людей.
top_15_streets_data = df.query('street in @top_15_streets.index')
Распределение заведений по категориям
plt.figure(figsize=(6, 15))
sns.countplot(y='street',
data=top_15_streets_data,
hue='category',
order=top_15_streets_data.groupby('street')['name'].count().sort_values(ascending=False).index
)
plt.title('Распределений заведений по категориям на топ-15 улиц')
plt.ylabel('Улица')
plt.xlabel('Количество точек');
На некоторых улицах преобладают заведения определенной категории (например, кафе на МКАДе), в то время как на других распределение более равномерное.
Далее отбираются и изучаются улицы, на которых находится только одно заведение общественного питания
only_1_streets = df.pivot_table(index='street',
values='name',
aggfunc='count'
).query('name == 1')
only_1_streets_data = df.query('street in @only_1_streets.index')
m_only_1 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
marker_cluster_2 = MarkerCluster().add_to(m_only_1)
def create_clusters_2(row):
for cat in df['category'].unique():
if row['category'] == cat:
icon_url = icons.get(cat)
icon = CustomIcon(icon_url, icon_size=(30, 30))
Marker([row['lat'],
row['lng']],
popup=f"{row['name']} {row['rating']}",
icon=icon,
).add_to(marker_cluster_2)
Данные объекты наносяся на карту:
only_1_streets_data.apply(create_clusters_2, axis=1)
m_only_1
Как видно, распределение этих объектов идентично распределению всех заведений.
Улицы, на которых они располагаются непольшие, поэтому на них и было открыло не более 1го заведения.
only_1_category_pivot = only_1_streets_data.pivot_table(index='category',
values='name',
aggfunc='count'
).sort_values(by='name',
ascending=False
)
only_1_category_pivot.columns = ['places_total']
only_1_category_pivot['percent'] = round(only_1_category_pivot['places_total'] / \
only_1_category_pivot['places_total'].sum() *100 , 2
)
only_1_category_pivot
| places_total | percent | |
|---|---|---|
| category | ||
| кафе | 159 | 34.79 |
| ресторан | 93 | 20.35 |
| кофейня | 84 | 18.38 |
| бар,паб | 39 | 8.53 |
| столовая | 36 | 7.88 |
| быстрое питание | 23 | 5.03 |
| пиццерия | 15 | 3.28 |
| булочная | 8 | 1.75 |
plt.figure(figsize=(12, 6))
sns.barplot(x=only_1_category_pivot['percent'],
y=only_1_category_pivot.index
)
plt.title('Распределение заведений по категориям на улицах с 1-м заведением (Относительные величины)')
plt.ylabel('Категория')
plt.xlabel('% заведений');
Распределение по категориям среди данных улиц похоже на общее распределение, однако доля кафе на них выше.
Исследуется зависимость среднего чека от различных параметров:
avg_bill_df = df.groupby('district', as_index=False)['middle_avg_bill'].agg('median')
avg_bill_df
| district | middle_avg_bill | |
|---|---|---|
| 0 | Восточный административный округ | 575.0 |
| 1 | Западный административный округ | 1000.0 |
| 2 | Северный административный округ | 650.0 |
| 3 | Северо-Восточный административный округ | 500.0 |
| 4 | Северо-Западный административный округ | 700.0 |
| 5 | Центральный административный округ | 1000.0 |
| 6 | Юго-Восточный административный округ | 450.0 |
| 7 | Юго-Западный административный округ | 600.0 |
| 8 | Южный административный округ | 500.0 |
Из таблице видно, что средний чек заведений в центре и на западе выше, однако следует визуализировать данную информацию:
m_chor_bill = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=avg_bill_df,
columns=['district', 'middle_avg_bill'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Медианный средний чек заведений по районам',
).add_to(m_chor_bill);
m_chor_bill
Отличие среднего чека в центре закономерно.
Высоки средний чек в Западном АО может объясняться вхождением в его состав Рублевского шоссе и аэропорта "Внуково"
plt.figure(figsize=(7, 12))
sns.barplot(y='category',
x='middle_avg_bill',
data=df,
order=df.groupby('category')['middle_avg_bill'].mean().sort_values(ascending=False).index
)
plt.title('Средний чек для каждой категории')
plt.ylabel('Тип заведения')
plt.xlabel('Средний чек');
Высокий средний чек ресторанов и баров объясняется как высоким классом обслуживания, так и тем, что их посещают в компании.
Столовые и точки продажи фаст-фуда в основном ориентированы на индивидуальных посетителей и предлагают более низкие цены, соответственно ниже и средний чек.
plt.figure(figsize=(15, 7))
sns.barplot(x='category',
y='middle_avg_bill',
data=df,
hue='chain',
order=df.groupby('category')['middle_avg_bill'].mean().sort_values(ascending=False).index
)
plt.title('Сравнение среднего чека сетевых и не сетевых заведений')
plt.xlabel('Тип заведения')
plt.ylabel('Средний чек');
Несетевые рестораны и бары в среднем дороже, вероятно это объясняется их уникальностью.
Среди кофеен и булочных наоборот, сети имеют возможность усатанвливать более высокие цены благодаря узнаваемости бренда.
В наборе данных представлено множество заведений общественного уровня, разного типа, уровня цен, локаций и.т.д
Заведения распределены по локациям достаточно равномерно, их плотность увеличивается ближе к центру города. Наибольшее количество заведений имеют категории "Кафе" и "Ресторан", ввиду их универсальности и широкого меню:
Также тип заведений в значительной мере зависит от того, где располагается объект.
Заведения в разных окрестных районах имеют схожие рейтинги
Также стоит отметить, что ценовая политика объектов различных категорий зависит от того, являются ли они сетевыми или индивидуальными.
Изложенные выше результаты в дальнейшем используются для составления бизнес-плана открытия новой кофейни.
Перед открытием нового заведения необходимо изучить и определить:
и другие ключевые параметры.
coffee = df.query('category == "кофейня"')
coffee_count = coffee.groupby('district', as_index=False)['name'].agg('count')
coffee_count
| district | name | |
|---|---|---|
| 0 | Восточный административный округ | 105 |
| 1 | Западный административный округ | 150 |
| 2 | Северный административный округ | 193 |
| 3 | Северо-Восточный административный округ | 159 |
| 4 | Северо-Западный административный округ | 62 |
| 5 | Центральный административный округ | 428 |
| 6 | Юго-Восточный административный округ | 89 |
| 7 | Юго-Западный административный округ | 96 |
| 8 | Южный административный округ | 131 |
coffee_count['name'].sum()
1413
Сводная таблица с количеством кофеен в каждом районе.
Ниже приведена ее визуализация
m_chor_coffee = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
Choropleth(
geo_data=state_geo,
data=coffee_count,
columns=['district', 'name'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Количество кофеен по районам'
).add_to(m_chor_coffee);
m_chor_coffee
Количество кофеен в центре заметно выше, чем в других районах.
Для выбора наиболее подходящего места следует также изучить данные о проходимости.
district_df_coffee = district_df.merge(coffee_count, on='district')
district_df_coffee.columns=('district', 'total_places', 'coffee_places')
district_df_coffee['coffee_percent'] = round(district_df_coffee['coffee_places'] / \
district_df_coffee['total_places'], 2)
district_df_coffee = district_df_coffee.sort_values(by='coffee_percent', ascending=False)
plt.figure(figsize=(12, 5))
sns.barplot(x=district_df_coffee['coffee_percent'],
y=district_df_coffee['district']
)
plt.title('Доля кофеен от числа объектов')
plt.xlabel('Соотношение')
plt.ylabel('Район');
Стоит оценить среднюю стоимость чашки кофе имеющихся кофеен для определения целевой цены:
Поскольку данные могут содержать "выбросы", данные стоит предварительно отфильтровать
sns.boxplot(coffee['middle_coffee_cup']);
Посмотрим на заведения, в которых встречаются "выбросы" в значении стоимости чашки кофе
coffee[coffee['middle_coffee_cup'] > 400]
| index | name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | is_24/7 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2856 | 2859 | шоколадница | кофейня | большая семёновская улица, 27, корп. 1 | Восточный административный округ | ежедневно, 08:00–23:00 | 55.782268 | 37.709022 | 4.2 | средние | Цена чашки капучино:230–2907 ₽ | NaN | 1568.0 | 1 | 48.0 | большая семёновская улица | 0 |
Судя по всему, в столбце avg_bill допущена опечатка, из-за чего и столбец middle_coffe_cup записан неверно.
Следует исправить эти записи.
coffee['avg_bill'][2856] = 'Цена чашки капучино:230–290 ₽'
coffee['middle_coffee_cup'][2856] = 260
plt.figure(figsize=(12, 6))
sns.histplot(coffee['middle_coffee_cup'], kde=True)
plt.title('Распределение числа кофеен по стоимости чашки кофе')
plt.xlabel('Стоимость 1 чашки кофе, р.')
plt.ylabel('Количество кофеен')
plt.show();
Из графика можно заметить 3-4 пика.
Вероятно, они соответствуют ценовым категориям данных кофеен.
Следует изучить ценовые категории, а также их расположение, что поможет в выборе локации.
price_category_pivot = coffee.pivot_table(index='price',
values='name',
aggfunc='count').sort_values(by='name', ascending=False)
price_category_pivot
| name | |
|---|---|
| price | |
| средние | 443 |
| низкие | 220 |
| высокие | 57 |
| выше среднего | 38 |
pie_price_category = go.Figure(data=[go.Pie(labels=price_category_pivot.index,
values=price_category_pivot['name'],
marker_colors=palette,
title = 'Распределение кофеен по ценовым категориям'
)
]
)
pie_price_category.show()
Кофеен с низкими и умеренными ценами заметно больше.
Далее следует изучить распределение кофеен по районам и ценовым категориям.
plt.figure(figsize=(7, 12))
sns.countplot(y='district',
data=coffee,
hue='price',
order=coffee.groupby('district')['category'].count().sort_values(ascending=False).index
)
plt.title('Распределение кофеен по районам и ценовым категориям')
plt.ylabel('Район, категория')
plt.xlabel('Количество точек');
Исходя из полученной информации можно сделать вывод, что необходимо связывать выбираемую локацию и планируемум ценовую категорию кофейни.
В некоторый районах преобладают кофейни с низкими ценами, в других - со средними.
В определенных районах кофейни определнных ценовых категорий не представлены вовсе
(стоит также уточнить, это показатель пустующей ниши или отсутствия спроса).
Исследуется количество кофеен, работающих ежедневно и круглосуточно.
Также изучается их распределение по районам.
coffee_24_7 = coffee[coffee['is_24/7'] == 1]
coffee_24_7_count = coffee_24_7.groupby('district',
as_index=False
)['name'].agg('count').sort_values(by='name',
ascending=False
)
coffee_24_7_count
| district | name | |
|---|---|---|
| 5 | Центральный административный округ | 26 |
| 1 | Западный административный округ | 9 |
| 7 | Юго-Западный административный округ | 7 |
| 0 | Восточный административный округ | 5 |
| 2 | Северный административный округ | 5 |
| 3 | Северо-Восточный административный округ | 3 |
| 4 | Северо-Западный административный округ | 2 |
| 6 | Юго-Восточный административный округ | 1 |
| 8 | Южный административный округ | 1 |
coffee_24_7_count['name'].sum()
59
Как видно из таблицы, кофеен с графиком 24/7 довольно мало.
Для выбора наиболее подходящих часов работы следует дополнительно изучить, связано ли это с отсутствием спроса
(вероятно, клиенты не заинтересованы в чашке кофе поздно вечером или ночью)
Исследуются значения рейтинга кофеен, расположенных в различных районах Москвы
coffee_rating = coffee.groupby('district', as_index=False)['rating'].agg('median')
coffee_rating
| district | rating | |
|---|---|---|
| 0 | Восточный административный округ | 4.3 |
| 1 | Западный административный округ | 4.2 |
| 2 | Северный административный округ | 4.3 |
| 3 | Северо-Восточный административный округ | 4.3 |
| 4 | Северо-Западный административный округ | 4.3 |
| 5 | Центральный административный округ | 4.3 |
| 6 | Юго-Восточный административный округ | 4.3 |
| 7 | Юго-Западный административный округ | 4.3 |
| 8 | Южный административный округ | 4.3 |
Данные практически идентичны, средний рейтинг кофеен - 4,3 балла.